/**
 * 
 */
package org.swing.utility.input.util;

import javax.swing.*;
import java.util.*;
import java.util.prefs.Preferences;
import java.io.*;
import java.awt.*;
import java.net.*;

/**
 * @author Le Quynh Nhu
 * 
 */
public class PropertyManager extends Properties {
	private final static String PRODUCT_NAME = "App"; // wants to be passed in
														// but would need to be
														// environment variable.
	private final static String PREFS_ROOT_NAME = "com.superliminal.userprefs";

	/**
	 * apps should load any user-specific property overrides directly into this
	 * object and call setProperty to customize.
	 */
	public static PropertyManager userprefs = new LocalProps(new File(
			getUserPrefFile()));
	private final static Properties sysprops = new PropertyManager(userprefs);

	/**
	 * apps should typically only call getProperty on this object although it is
	 * ok to store program arguments and other session overrides here too.
	 */
	public final static PropertyManager top = new PropertyManager(sysprops);

	static {
		init();
	} // to perform static initialization

	/**
	 * users have no business subclassing this class so a private empty
	 * constructor will forbid it.
	 */
	private PropertyManager() {
	}

	public PropertyManager(Properties defaults) {
		super(defaults);
	}

	/**
	 * Utility to load a properties object from a file.
	 * 
	 * @param prop_url
	 *            points to a property file
	 * @param into
	 *            is the Properties object to populate.
	 */
	public static void loadProps(URL prop_url, Properties into) {
		if (prop_url == null) {
			// System.err.println("PropertyManager.loadProps: passed null url");
			return;
		}
		try {
			URLConnection connection = prop_url.openConnection();
			InputStream propstream = connection.getInputStream();
			into.load(propstream);
		} catch (Exception e) {
			System.err
					.println("PropertyManager.init: Couldn't load property file '"
							+ prop_url.getPath()
							+ "'. Make sure this subpath can be found within the classpath.");
		}
	}

	/**
	 * Utility to load a properties object from a file.
	 * 
	 * @param prop_file_name
	 *            names a file expected to be found under ./resources
	 * @param into
	 *            is the Properties object to populate.
	 */
	public static void loadProps(String prop_file_name, Properties into) {
		String path = "resources" + File.separatorChar + prop_file_name;
		URL propurl = PropertyManager.class.getClassLoader().getResource(path);
		loadProps(propurl, into);
	}

	/**
	 * Utility to load a set of command line arguments into a Properties object.
	 * All argument names are expected to begin with a minus. If followed by an
	 * argument without a minus, that argument is taken as the value for the one
	 * with the minus. Arguments followed directly by another flagged argument
	 * are taken as boolean arguments whos values are set to the string "true".
	 * 
	 * @param args
	 *            typically an args array from a main method but with extracted
	 *            elements possibly nulled out.
	 * @param into
	 *            the Properties file to load into.
	 */
	public static void loadProps(String args[], Properties into) {
		for (int i = 0; i < args.length; i++) {
			if (args[i] == null)
				continue; // skip any nulled out elements (caller propably used
							// and extracted them)
			if (args[i].startsWith("-")) {
				// Make sure there's another arg
				if ((i + 1) < args.length) {
					// Make sure it's not another flag
					if (args[i + 1] == null || args[i + 1].startsWith("-")) {
						// Must be a flag without a value, set to "true"
						into.setProperty(args[i].substring(1), "true");
					} else {
						into.setProperty(args[i].substring(1), args[i + 1]);
						++i; // skip to next arg pair
					}
				} else {
					// Must be a flag without a value at the end of the args
					into.setProperty(args[i].substring(1), "true");
				}
			} else {
				// argument without a dash; must be malformed
				System.err.println("Invalid propertyfile argument: " + args[i]);
			}
		}
	}

	/**
	 * A specialized PropertyManager that loads from a given local file.
	 * Properties that are set on these objects are immediately persisted to
	 * that file regardless of whether it existed originally. This class is
	 * meant for handling user preferences that persist across sessions.
	 */
	private static class LocalProps extends PropertyManager {
		private File localPropFile;
		private boolean storeFailed = false;

		public LocalProps(File localPropFile) {
			this.localPropFile = localPropFile;
			if (!localPropFile.exists())
				return; // nothing to load
			try {
				load(new FileInputStream(localPropFile));
			} catch (IOException e) {
				System.err
						.println("PropertyManager.LocalProps: Could not load local prop file '"
								+ localPropFile.getAbsolutePath() + "'");
				this.localPropFile = null;
			}
		}

		public void setFile(File newFile) {
			localPropFile.renameTo(newFile);
			localPropFile = newFile;
			storeFailed = false;
			writeToFile();
		}

		/**
		 * Calls super.setProperty() and then immediately attempts to store the
		 * entire contents to the user's preference file.
		 */
		public Object setProperty(String key, String value) {
			Object ret = super.setProperty(key, value);
			writeToFile();
			return ret;
		}

		/**
		 * Calls super.clear() and then immediately attempts to empty the user's
		 * preference file.
		 */
		public void clear() {
			super.clear();
			writeToFile();
		}

		/**
		 * Calls super.remove() and then immediately attempts to store the
		 * entire contents to the user's preference file.
		 */
		public Object remove(Object key) {
			Object ret = super.remove(key);
			writeToFile();
			return ret;
		}

		/**
		 * Attempts to store the entire contents to the user's preference file.
		 */
		private void writeToFile() {
			if (localPropFile == null || storeFailed)
				return;
			try {
				FileOutputStream fos = new FileOutputStream(localPropFile);
				this.store(
						fos,
						PRODUCT_NAME
								+ " User Preferences -- The location of this file can be changed via Edit->Preferences");
				fos.close(); // important for renames to work
			} catch (IOException e) {
				storeFailed = true; // so as to only give fail msg once
				System.err
						.println("PropertyManager.LocalProps: Could not store local prop file '"
								+ localPropFile.getAbsolutePath() + "'");
				e.printStackTrace(); // crude exception handling but we can't
										// count on logging or other services
										// available.
			}
		}

		/**
		 * Overrides to return keys alphabeticaly so that the store method will
		 * also write them in that order. NOTE: The output of this method will
		 * always be sorted but for the sorted store operation to work depends
		 * upon the Java implementation continuing to call this method when
		 * iterating. That is likely but could possibly change.
		 * 
		 * @return alphabetically sorted key set.
		 */
		public Enumeration<Object> keys() {
			Vector keyList = new Vector();
			for (Enumeration keysEnum = super.keys(); keysEnum
					.hasMoreElements();)
				keyList.add(keysEnum.nextElement());
			Collections.sort(keyList);
			return keyList.elements();
		}
	} // end class LocalProps

	/**
	 * @return the path to where the user's preference file is or will be
	 *         living.
	 */
	public static String getUserPrefFile() {
		// String userdefault = new
		// File(javax.swing.filechooser.FileSystemView.getFileSystemView().getHomeDirectory(),
		// PRODUCT_NAME+".props").getAbsolutePath();
		File redsealdir = new File(System.getProperty("user.home")
				+ File.separator + "redseal");
		if (!redsealdir.exists())
			redsealdir.mkdir();
		String userdefault = new File(redsealdir, PRODUCT_NAME + ".props")
				.getAbsolutePath();
		return Preferences.userRoot().node(PREFS_ROOT_NAME)
				.get(PRODUCT_NAME, userdefault);
	}

	/**
	 * Sets the path for where user preferences will live.
	 * 
	 * @param newLoc
	 *            is a file path to where the user preferences should now live.
	 */
	public static void setUserPrefFile(String newLoc) {
		((LocalProps) userprefs).setFile(new File(newLoc));
		Preferences.userRoot().node(PREFS_ROOT_NAME).put(PRODUCT_NAME, newLoc);
	}

	/**
	 * High-level method that presents a file save dialog allowing the user to
	 * select a new location for their user preferences file.
	 * 
	 * @param parent
	 *            parent for the presented file chooser.
	 */
	public static void showPrefFileChooser(Component parent) {
		JFileChooser chooser = new JFileChooser(new File(getUserPrefFile()));
		// System.out.println("current file loc: " + getUserPrefFile());
		if (chooser.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) {
			String newloc = chooser.getSelectedFile().getAbsolutePath();
			// System.out.println("new location: " + newloc);
			setUserPrefFile(newloc);
		}
	}

	/**
	 * A specialized PropertyManager that attempts to load a set of properties
	 * from a URL representing a set of VAR, reseller, or customer-specific
	 * overrides for an application's default property values. See the
	 * ClientProp documentation above for descriptions of the subtle syntax
	 * differences for file locations and quoted keys.
	 */
	private static class RemoteProps extends Properties {
		private String prefix;

		public RemoteProps(String fileurl, Properties def) {
			defaults = def;
			if (fileurl == null)
				return;
			URL url = null;
			try {
				url = new URL(fileurl);
			} catch (MalformedURLException e) {
				System.err.println("Couldn't open remote property file: "
						+ fileurl);
			}
			if (url != null)
				loadProps(url, this);
			prefix = fileurl.substring(0, fileurl.lastIndexOf('/'));
		}

		/**
		 * @return normal getProperty value except that for values beginning
		 *         with '/', prepends url path refering to same directory as the
		 *         remote property file given to the constructor. Also, quoted
		 *         values will have their quotes striipped. This is in case a
		 *         user wants to create a path or other property beginning with
		 *         '/'.
		 */
		public String getProperty(String key) {
			// for debugging vendor props, if uncommented, this will provide a
			// good line
			// on which to set a breakpoint:
			// if(key.equalsIgnoreCase("main.logo.small"))
			// key = key;
			String val = (String) get(key);
			if (val == null)
				return defaults.getProperty(key);
			if (val.startsWith("/")) // path is relative to vendor prop file
										// location
				val = prefix + val;
			if (val.startsWith("\"") && val.endsWith("\"")) // quoted strings
															// for root paths or
															// to contain spaces
				val = val.substring(1, val.length() - 1);
			return val;
		}

		public String getProperty(String key, String dflt) {
			String val = getProperty(key);
			return val == null ? dflt : val;
		}
	} // end class RemoteProps

	/**
	 * Static initializer to bootstrap the system.
	 */
	private static void init() {
		Properties sys = System.getProperties();
		for (Enumeration e = sys.keys(); e.hasMoreElements();) {
			String key = (String) e.nextElement();
			sysprops.setProperty(key, sys.getProperty(key));
			// System.out.println(key + " = " + sys.getProperty(key));
		}
		PropertyManager defs = new PropertyManager();
		loadProps("defaults.prop", defs);
		// load any vendor-specific property file specified.
		// note, for testing, you can set a vm environment variable via
		// something like:
		// -Dvendorprops=file:///C|/Superliminal/MC4D/resources/vendor.prop
		// or just hardcode with a line like this:
		// vendorpropfile =
		// "file:///C|/Superliminal/MC4D/resources/vendor.prop";
		String vendorpropfile = System.getProperty("vendorprops");
		Properties vendorprops = new RemoteProps(vendorpropfile, defs);
		userprefs.defaults = new PropertyManager(vendorprops);
	}

	/**
	 * Helper function to retrive integer values from the top properties.
	 * 
	 * @param key
	 *            property name
	 * @param def
	 *            default value
	 * @return integer value of top.getProperty(key) or default if not found or
	 *         parsed.
	 */
	public static int getInt(String key, int def) {
		try {
			return Integer.parseInt(top.getProperty(key));
		} catch (NumberFormatException nfe) {
		}
		return def;
	}

	/**
	 * Helper function to retrive float values from the top properties.
	 * 
	 * @param key
	 *            property name
	 * @param def
	 *            default value
	 * @return float value of top.getProperty(key) or default if not found or
	 *         parsed.
	 */
	public static float getFloat(String key, float def) {
		try {
			return Float.parseFloat(top.getProperty(key));
		} catch (Exception e) {
		}
		return def;
	}

	/**
	 * Helper function to retrive boolean values from the top properties.
	 * 
	 * @param key
	 *            property name
	 * @param def
	 *            default value
	 * @return boolean value of top.getProperty(key) or default if not found or
	 *         parsed.
	 */
	public static boolean getBoolean(String key, boolean def) {
		try {
			String topval = top.getProperty(key);
			if (topval == null)
				return def;
			return Boolean.parseBoolean(topval);
		} catch (Exception e) {
		}
		return def;
	}

	/**
	 * Helper function to retrive color objects from properties with the format
	 * r,g,b where each channel is a value from 0 to 255. The color can also be
	 * in hexadecimal format if it begins with "#", e.g., "#aabbcc"
	 * 
	 * @param key
	 *            the color property name
	 * @return a Color object with the parsed red, green, and blue values.
	 */
	public static Color getColor(String key, Color def) {
		String str = top.getProperty(key);
		if (str == null)
			return def;
		if (str.indexOf('#') >= 0)
			return Color.decode(str);

		StringTokenizer toc = new StringTokenizer(str, ",");
		int r = Integer.parseInt(toc.nextToken());
		int g = Integer.parseInt(toc.nextToken());
		int b = Integer.parseInt(toc.nextToken());
		return new Color(r, g, b);
	}

	public static Color getColor(String key) {
		return getColor(key, null);
	}

	/**
	 * Utility to construct a simple http url from keys to host and port
	 * properties.
	 * 
	 * @return an http url string.
	 */
	public static String getURL(String hostkey, String portkey) {
		return "http://" + top.getProperty(hostkey) + ":"
				+ top.getProperty(portkey) + "/";
	}

	/**
	 * Simple example program.
	 */
	public static void main(String args[]) {
		// test of setting a top-level property, e.g. a setting specified on
		// command line.
		PropertyManager.top.setProperty("debugging", "true");
		// get a bunch of application values from the property files
		System.out.println("main.background = " + getColor("main.background"));
		System.out.println("tables.header.background = "
				+ top.getProperty("tables.header.background") + " -> "
				+ getColor("tables.header.background"));
		System.out.println("title = "
				+ PropertyManager.top.getProperty("main.title"));
		// get a system property
		System.out.println("os.name = "
				+ PropertyManager.top.getProperty("os.name"));
		// get a true top level (i.e. command line) property
		System.out.println("debugging = "
				+ PropertyManager.top.getProperty("debugging"));
		int runcount = PropertyManager.getInt("test.runcount", 0);
		System.out.println("test.runcount = " + runcount);
		PropertyManager.userprefs.setProperty("test.runcount", ++runcount + "");
		System.out.println("test.runcount now = "
				+ PropertyManager.userprefs.getProperty("test.runcount"));
		PropertyManager.userprefs.clear(); // testing that clearing userprefs
											// writes the file
		System.out.println("num pref keys after clear: "
				+ PropertyManager.userprefs.size());
		PropertyManager.userprefs.setProperty("test.runcount", runcount + "");
		showPrefFileChooser(null);
	}
}
